﻿namespace Example.Cryptography
{
    using System;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.Security.Cryptography;
    using System.Xml;
    using System.Xml.XPath;

    public sealed class TripleDesSettings : IDisposable
    {
        private TripleDESCryptoServiceProvider _algorithm;

        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "This object disposes the algorithm.")]
        public TripleDesSettings()
            : this(new TripleDESCryptoServiceProvider())
        {
        }

        public TripleDesSettings(string config)
            : this(ToTripleDes(config))
        {
        }

        public TripleDesSettings(IXPathNavigable config)
            : this(ToTripleDes(config))
        {
        }

        public TripleDesSettings(string key, 
                                 string iv)
            : this(Convert.FromBase64String(key), Convert.FromBase64String(iv))
        {
        }

        public TripleDesSettings(byte[] key, 
                                 byte[] iv)
            : this(ToTripleDes(key, iv))
        {
        }

        public TripleDesSettings(TripleDESCryptoServiceProvider algorithm)
        {
            Algorithm = algorithm;
        }

        ~TripleDesSettings()
        {
            Dispose(false);
        }

        private TripleDESCryptoServiceProvider Algorithm
        {
            get
            {
                return _algorithm;
            }

            set
            {
                if (null == value)
                {
                    throw new ArgumentNullException("value");
                }

                _algorithm = value;
            }
        }

        [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "ciphertext", Justification = "The casing is correct.")]
        public TripleDesPlaintext Decrypt(TripleDesCiphertext ciphertext)
        {
            if (null == ciphertext)
            {
                throw new ArgumentNullException("ciphertext");
            }

            TripleDesPlaintext plaintext;

            using (var crypto = Algorithm.CreateDecryptor())
            {
                using (var memory = new MemoryStream(ciphertext.ToBytes()))
                {
                    using (var stream = new CryptoStream(memory, crypto, CryptoStreamMode.Read))
                    {
                        using (var reader = new StreamReader(stream))
                        {
                            plaintext = new TripleDesPlaintext(reader.ReadToEnd());
                        }
                    }
                }
            }

            return plaintext;
        }

        public TripleDesCiphertext Encrypt(TripleDesPlaintext plaintext)
        {
            if (null == plaintext)
            {
                throw new ArgumentNullException("plaintext");
            }

            TripleDesCiphertext ciphertext;

            using (var crypto = Algorithm.CreateEncryptor())
            {
                using (var memory = new MemoryStream())
                {
                    using (var stream = new CryptoStream(memory, crypto, CryptoStreamMode.Write))
                    {
                        var plainByte = plaintext.ToBytes();

                        stream.Write(plainByte, 0, plainByte.Length);
                        stream.FlushFinalBlock();
                        memory.Position = 0;

                        ciphertext = new TripleDesCiphertext(memory.ToArray());
                    }
                }
            }

            return ciphertext;
        }

        public override string ToString()
        {
            var key = Convert.ToBase64String(Algorithm.Key);
            var iv = Convert.ToBase64String(Algorithm.IV);

            return string.Format(CultureInfo.InvariantCulture, "<triple-des key=\"{0}\" iv=\"{1}\"/>", key, iv);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private static TripleDESCryptoServiceProvider ToTripleDes(string config)
        {
            if (null == config)
            {
                throw new ArgumentNullException("config");
            }

            var xml = new XmlDocument();
            xml.LoadXml(config);

            return ToTripleDes(xml.DocumentElement);
        }

        private static TripleDESCryptoServiceProvider ToTripleDes(IXPathNavigable config)
        {
            string key = null;
            string iv = null;

            if (null == config)
            {
                throw new ArgumentNullException("config");
            }

            var iterator = config.CreateNavigator().Select("/triple-des/@key");
            if (iterator.MoveNext() && null != iterator.Current)
            {
                key = iterator.Current.Value;
            }

            iterator = config.CreateNavigator().Select("/triple-des/@iv");
            if (iterator.MoveNext() && null != iterator.Current)
            {
                iv = iterator.Current.Value;
            }

            return ToTripleDes(key, iv);
        }

        private static TripleDESCryptoServiceProvider ToTripleDes(string key, 
                                                                  string iv)
        {
            return ToTripleDes(Convert.FromBase64String(key), Convert.FromBase64String(iv));
        }

        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "It is the responsibility of the calling code to dispose.")]
        private static TripleDESCryptoServiceProvider ToTripleDes(byte[] key, 
                                                                  byte[] iv)
        {
            var result = new TripleDESCryptoServiceProvider
                             {
                                 Key = key, 
                                 IV = iv
                             };

            return result;
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                Algorithm.Dispose();
            }
        }
    }
}